package exserver

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"runtime"
	"strings"
	"syscall"
	"time"

	"gitee.com/oscstudio/gitee"
	"github.com/gliderlabs/ssh"
)

// warning: this ExServer only for linux
var (
	// the signal will be handle
	hookableSignals = []os.Signal{
		syscall.SIGTERM,
		syscall.SIGUSR1,
		syscall.SIGUSR2,
	}
)

type ExServer struct {
	Name               string // server name, like banjo or basalt
	Srv                exServe
	PIDFile            string
	IdleConnsClosed    chan struct{} // flag to safe close server
	listener           net.Listener  // socketFd
	isRestartProcess   bool
	isAddrChanged      bool
	restartProcessFlag string // flag to this process is the restart process?
	oldAddrFlag        string // flag to restart process's Addr is changed?
}

type exServe interface {
	Serve(l net.Listener) error
	Shutdown(ctx context.Context) error
}

func NewExServer(name string, pidFile string, srv exServe) *ExServer {
	// set restart-process-flag and old-addr-flag to env for new process
	restartProcessFlag := gitee.StrCat(strings.ToUpper(name), "_RESTART_PROCESS")
	oldAddrFlag := gitee.StrCat(strings.ToUpper(name), "_OLD_ADDR")

	oa := os.Getenv(oldAddrFlag)
	s := &ExServer{
		Name:             name,
		Srv:              srv,
		PIDFile:          pidFile,
		IdleConnsClosed:  make(chan struct{}),
		isRestartProcess: os.Getenv(restartProcessFlag) != "",
		// isAddrChanged:      oa != "" && oa != srv.Addr,
		restartProcessFlag: restartProcessFlag,
		oldAddrFlag:        oldAddrFlag,
	}

	addr, _ := s.getAddr()
	s.isAddrChanged = oa != "" && oa != addr
	return s
}

func (exSrv *ExServer) ListenAndServeEx() error {
	var err error
	for i := 0; i < 20; i++ {
		err = exSrv.listenAndServe()
		if !exSrv.isAddressInUse(err) {
			break
		}
		time.Sleep(time.Millisecond * 100)
	}

	if errors.Is(err, http.ErrServerClosed) {
		<-exSrv.IdleConnsClosed
		return nil
	}

	if errors.Is(err, ssh.ErrServerClosed) {
		<-exSrv.IdleConnsClosed
		return nil
	}

	return err
}

func (exSrv *ExServer) ListenAndServeTLSEx(certFile, keyFile string) error {
	if _, ok := exSrv.Srv.(*http.Server); !ok {
		return errors.New("ListenAndServeTLSEx only support http server")
	}

	var err error
	for i := 0; i < 20; i++ {
		err = exSrv.listenAndServeTLS(certFile, keyFile)
		if !exSrv.isAddressInUse(err) {
			break
		}
		time.Sleep(time.Millisecond * 100)
	}
	if err != http.ErrServerClosed {
		return err
	}

	<-exSrv.IdleConnsClosed
	return nil
}

func (exSrv *ExServer) getAddr() (string, error) {
	var addr string
	switch exSrv.Srv.(type) {
	case *http.Server:
		addr = exSrv.Srv.(*http.Server).Addr
	case *ssh.Server:
		addr = exSrv.Srv.(*ssh.Server).Addr
	default:
		return "", errors.New("Illegal Serve type")
	}
	return addr, nil
}

func (exSrv *ExServer) listenAndServe() error {
	addr, err := exSrv.getAddr()
	if err != nil {
		return err
	}

	if addr == "" {
		addr = ":http"
	}

	go exSrv.handleSignals()

	ln, err := exSrv.getListener(addr)
	if err != nil {
		return err
	}

	if exSrv.listener == nil {
		exSrv.listener = ln
	}

	if exSrv.isRestartProcess {
		gitee.AppExit(exSrv.Name, gitee.StrCat(exSrv.PIDFile, ".old"), false)
	}

	return exSrv.Srv.Serve(ln)
}

func (exSrv *ExServer) listenAndServeTLS(certFile, keyFile string) error {
	addr, err := exSrv.getAddr()
	if err != nil {
		return err
	}

	if addr == "" {
		addr = ":https"
	}

	go exSrv.handleSignals()

	ln, err := exSrv.getListener(addr)
	if err != nil {
		return err
	}

	return exSrv.Srv.(*http.Server).ServeTLS(ln, certFile, keyFile)
}

func (exSrv *ExServer) getListener(addr string) (ln net.Listener, err error) {
	// if it's restart process And addr doesn't changed
	// use the socketFd from old process
	if exSrv.isRestartProcess && !exSrv.isAddrChanged {
		// TODO use exist listener
		socketFd := os.NewFile(3, "")
		defer socketFd.Close()
		if ln, err = net.FileListener(socketFd); err != nil {
			err = fmt.Errorf("net.FileListener error: %v", err)
		}
		return
	}

	// if restart proccess but addr is changed
	// close the socketFd from old process
	if exSrv.isAddrChanged {
		socketFd := os.NewFile(3, "")
		defer socketFd.Close()
	}
	if ln, err = net.Listen("tcp", addr); err != nil {
		return
	}
	return
}

//IsAddressInUse bind: address already in use
func (exSrv *ExServer) isAddressInUse(err error) bool {
	errOpError, ok := err.(*net.OpError)
	if !ok {
		return false
	}
	errSyscallError, ok := errOpError.Err.(*os.SyscallError)
	if !ok {
		return false
	}
	errErrno, ok := errSyscallError.Err.(syscall.Errno)
	if !ok {
		return false
	}
	if errErrno == syscall.EADDRINUSE {
		return true
	}
	const WSAEADDRINUSE = 10048
	if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
		return true
	}
	return false
}

// get this process's socketFd
func (exSrv *ExServer) getListenerFile() *os.File {
	tl := exSrv.listener.(*net.TCPListener)
	fl, _ := tl.File()
	return fl
}

func (exSrv *ExServer) handleSignals() {
	var sig os.Signal
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, hookableSignals...)
	for {
		sig = <-sigChan
		switch sig {
		case syscall.SIGTERM:
			exSrv.shutdown()
		case syscall.SIGUSR1:
			exSrv.shutdown()
		case syscall.SIGUSR2:
			// TODO restart
			exSrv.fork()
		}
	}
}

func (exSrv *ExServer) shutdown() error {
	ctx, cancel := context.WithCancel(context.Background())
	defer func() {
		cancel()
	}()
	if err := exSrv.Srv.Shutdown(ctx); err != nil {
		return err
	}
	close(exSrv.IdleConnsClosed)
	return nil
}

func (exSrv *ExServer) fork() error {
	addr, err := exSrv.getAddr()
	if err != nil {
		return err
	}

	env := append(
		os.Environ(),
		gitee.StrCat(exSrv.restartProcessFlag, "=1"),
		gitee.StrCat(exSrv.oldAddrFlag, "=", addr),
	)

	files := make([]*os.File, 1)
	files[0] = exSrv.getListenerFile()
	path := os.Args[0]
	var args []string
	if len(os.Args) > 1 {
		args = os.Args[1:]
	}

	cmd := exec.Command(path, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.ExtraFiles = files
	cmd.Env = env

	os.Rename(exSrv.PIDFile, exSrv.PIDFile+".old")
	// don't use cmd.Run(), we start a daemon process
	cmd.Start()
	return nil
}
